package tiasw;

/**
 * Title:        Turnout In a Small World
 * Description:  Program to study how neighborhood influence propagates turnout behavior.
 * Copyright:    Copyright (c) 2002
 * Company:      Harvard University
 * @author James Fowler
 * @version 1.0
 */

import uchicago.src.sim.engine.*;
import uchicago.src.sim.network.*;
import uchicago.src.sim.util.SimUtilities;
import java.util.*;
import java.text.*;

public class Model extends SimpleModel {

  // Model variables
  ArrayList citizens;
  int numCitizens;
  int numDiscussions;
  int aveDegree;  // must be an even number
  double rewireRate; // sets frequency of shortcuts
  double corr;  // sets rate of correlation in citizen preferences
  double clustCoef; // clustering coefficient
  int numClustCoefSample; // number of citizens to sample when calculating clustcoef
  double prefCorr; // preference correlation
  double prefCorr2;
  int numPrefCorrSample;
  double avePathLength;
  int numAvePathLengthSample;
  double preference;
  boolean turnout;
  double turnoutRate;
  double imitationRate;
  int leftTotal;
  int rightTotal;
  int leftTotalNV;
  int rightTotalNV;
  int dLeft;
  int dRight;
  int net;
  double leftPlatform;
  double rightPlatform;
  int egoID;
  long seed;
  int egoVote;
  int egoDegree;
  double egoClustCoef;
  double egoPreferenceCorr;
  int netTotal;
  int numEgoSample;
  int egoSample[];

/////////////////////////////////////////////////////////////////////////
  // Initializing the model /////////////////////////////////////////////

  public void setup() {
    // Call the setup() method of the parent first
    super.setup();

    // Setting the name of our model
    name = "Turnout In a Small World";

    // Setting the values of the model parameters
    // Note that with the presence of the GUI, these are only
    // _default_ values.

    numCitizens = 10000;  // number of citizens
    numDiscussions = 20; // number of discussions with each neighbor
    aveDegree=4;  // number of neighbors (must be an even!)
    rewireRate=0.08; // frequency of shortcuts
    corr=0.6;  // 0=no preference correlation, 1= most correlation
    turnoutRate=0.5; // initial rate of turnout
    imitationRate=0.05; // likelihood that one neighbor copies another neighbor
    leftPlatform=0.4; // left party platform (fixed)
    rightPlatform=0.6; // right party platform (fixed)
    seed=(int)(Math.random()*100000.0)+1; // set random seed with random number
    netTotal=0; // initialize

    // variables setting the number of samples to take for statistic
    numEgoSample=100;
    egoSample = new int[numEgoSample];
    numAvePathLengthSample=100;
    numClustCoefSample=10000;
    numPrefCorrSample=10000;

  }

/////////////////////////////////////////////////////////////////////////

  // The method to build the Model's internals //////////////////////////
  public void buildModel() {

    // Set the number of counterfactuals to perform
    setStoppingTime(numEgoSample);

    // create the citizens
    citizens = new ArrayList();
    for (int i = 0; i < numCitizens; i++) {
      // generate citizen preference
      preference=corr*((double)i/(double)numCitizens)+(1.0-corr)*getNextDoubleFromTo(0.0,1.0);
      // assign initial turnout behavior
      turnout=(getNextDoubleFromTo(0.0,1.0)<turnoutRate);
      // create new citizen and add to list
      Citizen citizen = new Citizen(i, preference, turnout);
      citizens.add(citizen);
    }

    // Lattice: iterate through the citizens creating ties between
    // this citizen and its next few neghbors.
    int newNeighbor;
    for (int i = 0; i < numCitizens; i++) {
      Citizen citizen = (Citizen)citizens.get(i);
      for (int j = 0; j < aveDegree/2; j++) {
        // link to jth neighbor of i
        newNeighbor=(i+j+1)%numCitizens;
        Citizen neighbor = (Citizen)citizens.get(newNeighbor);
        createTie(citizen, neighbor);
      }
    }

    // Short-cuts: rewire a random number of extra ties across the graph

    // adjust rewireThreshold to account for the fact that each tie has two
    // chances of being rewired
    double rewireThreshold=1.0-Math.sqrt(1.0-rewireRate);
    // loop through citizens
    for (int i = 0; i < numCitizens; i++) {
      Citizen citizen = (Citizen)citizens.get(i);
      // loop backwards through all but first neighbor (to prevent disconnected graph)
      for (int j = citizen.getNumNeighbors() ; j >1  ; j--) {
        Citizen delNeighbor = (Citizen)citizen.neighbors.get(j-1);
        // check if this neighbor is solitary if edge deleted
        // if not, draw random number to see if it will be rewired
        if (delNeighbor.getNumNeighbors()<2 || getNextDoubleFromTo(0.0,1.0)>rewireThreshold)
          continue;                // skip to end of j loop
        boolean tryAgain;
        do {
          tryAgain=false;
          // generate new neighbor
          newNeighbor=getNextIntFromTo(0,numCitizens-1);
          // redo if this is self
          if (newNeighbor==i) {
            tryAgain=true;
          }
          // redo if this duplicates a current neighbor
          for (int k = 0; k < citizen.getNumNeighbors(); k++) {
            Citizen neighbor = (Citizen)citizen.neighbors.get(k);
            if (newNeighbor==neighbor.id)
              tryAgain=true;
          }
        } while(tryAgain);
        // remove existing tie
        citizen.removeNeighbor(delNeighbor);
        delNeighbor.removeNeighbor(citizen);
        // add new tie
        Citizen neighbor = (Citizen)citizens.get(newNeighbor);
        createTie(citizen, neighbor);
      }
    }

    // calculate some statistics for the network
    avePathLength=sampleAvePathLength(numAvePathLengthSample);
    clustCoef=sampleClustCoef(numClustCoefSample);
    prefCorr=samplePrefCorr(numPrefCorrSample);
    prefCorr2=samplePrefCorr(numPrefCorrSample);

    // Report some statistics
    reportResults();

    // create random sample pool of individuals to sample for counterfactual
    for (int i = 0; i < numEgoSample; i++) {
      egoSample[i]=getNextIntFromTo(0,numCitizens-1);
    }
  }

  /////////////////////////
  // Methods for buildModel
  /////////////////////////

  // create tie between two citizens
  public void createTie(Citizen citizen, Citizen neighbor) {
    citizen.addNeighbor(neighbor);
    neighbor.addNeighbor(citizen);
  }

  // Printing results to the screen
  public void reportResults() {
    // printNodeConnections(); // Use this to see who is connected to who
    System.out.println("Average Path Length: "+" "+avePathLength);// tNk
    System.out.println("Clustering Coefficient: "+" "+clustCoef); // t
    System.out.println("Preference Correlation: "+prefCorr); // 2t
    System.out.println("First Order Turnout Correlation: "+sampleTurnoutCorr(10000));
    System.out.println("Second Order Turnout Correlation: "+sampleTurnoutCorr2(10000));
  }

  // diagnostic for showing which nodes are connected to which
  public void printNodeConnections() {
    for (int i = 0; i < numCitizens; i++) {
      Citizen citizen = (Citizen)citizens.get(i);
      System.out.print(citizen.id+": "+citizen.preference+" ("+citizen.getNumNeighbors()+") ");
      for (int j = 0; j < citizen.getNumNeighbors(); j++) {
        Citizen neighbor = (Citizen)citizen.neighbors.get(j);
        System.out.print(neighbor.id+ " ");
      }
      System.out.println();
    }
  }

  // calculate first order preference Correlation
  public double samplePrefCorr(int t) {  // t = number of trials
    double prefC[]=new double[t];
    double prefN[]=new double[t];
    double meanC=0.0;
    double meanN=0.0;
    double sumCN=0.0;
    double sumC2=0.0;
    double sumN2=0.0;
    if (t>numCitizens)
      t=numCitizens;
    for (int i = 0; i < t; i++) {
      Citizen citizen = (Citizen)citizens.get(getNextIntFromTo(0,numCitizens-1));
      Citizen neighbor = (Citizen) citizen.neighbors.get(getNextIntFromTo(0,citizen.getNumNeighbors()-1));
      prefC[i]=citizen.preference;
      prefN[i]=neighbor.preference;
      meanC+=citizen.preference;
      meanN+=neighbor.preference;
    }
    meanC=meanC/(double)t;
    meanN=meanN/(double)t;
    for (int i = 0; i < t; i++) {
      sumCN+=(meanC-prefC[i])*(meanN-prefN[i]);
      sumC2+=(meanC-prefC[i])*(meanC-prefC[i]);
      sumN2+=(meanN-prefN[i])*(meanN-prefN[i]);
    }
    return sumCN/Math.sqrt(sumC2*sumN2);
  }

  // calculate first order turnout correlation
  public double sampleTurnoutCorr(int t) {  // t = number of trials
    int turnoutC[]=new int[t];
    int turnoutN[]=new int[t];
    double meanC=0.0;
    double meanN=0.0;
    double sumCN=0.0;
    double sumC2=0.0;
    double sumN2=0.0;
    if (t>numCitizens)
      t=numCitizens;
    for (int i = 0; i < t; i++) {
      Citizen citizen = (Citizen)citizens.get(getNextIntFromTo(0,numCitizens-1));
      Citizen neighbor = (Citizen)citizen.neighbors.get(getNextIntFromTo(0,citizen.getNumNeighbors()-1));
      if (citizen.turnout)
        turnoutC[i]=1;
      else
        turnoutC[i]=0;
      if (neighbor.turnout)
        turnoutN[i]=1;
      else
        turnoutN[i]=0;
      meanC+=(double)turnoutC[i];
      meanN+=(double)turnoutN[i];
    }
    meanC=meanC/(double)t;
    meanN=meanN/(double)t;
    for (int i = 0; i < t; i++) {
      sumCN+=(meanC-turnoutC[i])*(meanN-turnoutN[i]);
      sumC2+=(meanC-turnoutC[i])*(meanC-turnoutC[i]);
      sumN2+=(meanN-turnoutN[i])*(meanN-turnoutN[i]);
    }
    return sumCN/Math.sqrt(sumC2*sumN2);
  }

    // calculate second order turnout correlation
  public double sampleTurnoutCorr2(int t) {  // t = number of trials
    int turnoutC[]=new int[t];
    int turnoutN[]=new int[t];
    int tCounter=0;
    double meanC=0.0;
    double meanN=0.0;
    double sumCN=0.0;
    double sumC2=0.0;
    double sumN2=0.0;
    if (t>numCitizens)
      t=numCitizens;
    for (int i = 0; i < t; i++) {
      ArrayList neighborNeighbors=new ArrayList();
      Citizen citizen = (Citizen)citizens.get(getNextIntFromTo(0,numCitizens-1));
      Citizen neighbor = (Citizen)citizen.neighbors.get(getNextIntFromTo(0,citizen.getNumNeighbors()-1));
      Citizen neighborsNeighbor = (Citizen)neighbor.neighbors.get(getNextIntFromTo(0,neighbor.getNumNeighbors()-1));
      if (neighborsNeighbor.id==citizen.id)
        continue;
      if (citizen.turnout)
        turnoutC[tCounter]=1;
      else
        turnoutC[tCounter]=0;
      if (neighborsNeighbor.turnout)
        turnoutN[tCounter]=1;
      else
        turnoutN[tCounter]=0;
      meanC+=(double)turnoutC[tCounter];
      meanN+=(double)turnoutN[tCounter];
    tCounter++;
    }
    meanC=meanC/(double)tCounter;
    meanN=meanN/(double)tCounter;
    for (int i = 0; i < tCounter; i++) {
      sumCN+=(meanC-turnoutC[i])*(meanN-turnoutN[i]);
      sumC2+=(meanC-turnoutC[i])*(meanC-turnoutC[i]);
      sumN2+=(meanN-turnoutN[i])*(meanN-turnoutN[i]);
    }
    return sumCN/Math.sqrt(sumC2*sumN2);
  }


  // calculate clustering coefficient
  public double sampleClustCoef(int t) {  // t = number of trials
    int triangles=0;
    int connectedTriples=0;
    if (t>numCitizens)
      t=numCitizens;
    for (int i = 0; i < t; i++) {
      ArrayList neighborNeighbors=new ArrayList();
      Citizen citizen = (Citizen)citizens.get(getNextIntFromTo(0,numCitizens-1));
      Citizen neighbor = (Citizen)citizen.neighbors.get(getNextIntFromTo(0,citizen.getNumNeighbors()-1));
      for (int j = 0; j < neighbor.getNumNeighbors(); j++) {
        neighborNeighbors.add((Citizen)neighbor.neighbors.get(j));
      }
      neighborNeighbors.remove(citizen);  // remove citizen from list of neighbor's neighbors
      if (neighborNeighbors.size()>0) {  // make sure there is a neighbor left
        connectedTriples++;
        Citizen neighborsNeighbor = (Citizen) neighborNeighbors.get(getNextIntFromTo(0,neighborNeighbors.size()-1));
        if(neighborsNeighbor.neighbors.contains(citizen)) {
          triangles++;
        }
      }
    }
    return (double)triangles/(double)connectedTriples;
  }

  // calculate average path length
  public double sampleAvePathLength(int t) { // t = number of trials
    int x=0;
    double sumPathLength=0.0;
    if (t>numCitizens)
      t=numCitizens;
    // we must use try method because non-connected graphs cause
    // IndexOutOfBounds exception
    try {
      for (int i = 0; i < t; i++) {
        ArrayList searchBoundaryList=new ArrayList();
        // reset pathlength array
        int pathLength[]=new int[numCitizens];
        for (int j = 0; j < numCitizens; j++) {
          pathLength[j]=0;
        }
        Citizen initialCitizen = (Citizen)citizens.get(getNextIntFromTo(0,numCitizens-1));
        searchBoundaryList.add(initialCitizen);
        for (int j = 0; j < numCitizens; j++) {
          Citizen citizen = (Citizen)searchBoundaryList.get(j);
          for (int k = 0; k < citizen.getNumNeighbors(); k++) {
            Citizen neighbor = (Citizen)citizen.neighbors.get(k);
            if (pathLength[neighbor.id]==0) {
              pathLength[neighbor.id]=pathLength[citizen.id]+1;
              searchBoundaryList.add(neighbor);
              sumPathLength=sumPathLength+(double)pathLength[neighbor.id];
            }
          }
        }
      }
    } catch(IndexOutOfBoundsException e) {
      sumPathLength=0;
    }
    return (double)sumPathLength/((double)numCitizens*(double)t);
  }

/////////////////////////////////////////////////////////////////////////
//  Iterated methods ////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////

  // The main activity of the time step /////////////////////////////////
  public void step() {

      // choose next citizen in sample
      egoID=egoSample[(int)getTickCount()-1];

      // ego does not vote
      setEgo(false);
      setRngSeed(seed);
      discussions();
      election();
      recordNoVoteStats();
      resetElectorate();

      // ego votes in counterfactual election
      setEgo(true);
      setRngSeed(seed);
      discussions();
      election();
      updateStats();
      reportElection();
      resetElectorate();
  }

  // Sub-Activity: Discussions
  public void setEgo(boolean b) {
    for (int i = 0; i < numCitizens; i++) {
      Citizen citizen = (Citizen)citizens.get(i);  // loop through citizens
      // set ego citizen behavior
      if (citizen.id==egoID) {
        citizen.setTurnout(b);
        // calculate statistics if this is first call to ego
        if (!b) {
          egoVote=citizen.getVote(leftPlatform, rightPlatform);
          egoDegree=citizen.getNumNeighbors();
          // calculate ego clustering coefficient
          int triangles=0;
          int connectedTriples=0;
          for (int j = 0; j < egoDegree-1; j++) {
            Citizen neighbor1 = (Citizen)citizen.neighbors.get(j);
            for (int k = j+1; k < egoDegree; k++) {
              Citizen neighbor2 = (Citizen)citizen.neighbors.get(k);
              connectedTriples++;
              if(neighbor1.neighbors.contains(neighbor2))
                triangles++;
            }
          }
          egoClustCoef=(double)triangles/(double)connectedTriples;
        }
      }
    }
  }


  // Sub-Activity: Discussions
  public void discussions() {
    // number of discussions = N*k*q*D
    for (int n = 0; n<numDiscussions*numCitizens*aveDegree*imitationRate; n++) {
      // choose citizen at random
      Citizen citizen = (Citizen)citizens.get(getNextIntFromTo(0,numCitizens-1));
      Citizen neighbor = (Citizen)citizen.neighbors.get(getNextIntFromTo(0,citizen.getNumNeighbors()-1));
      if (neighbor.id!=egoID)  // keep ego from being changed
        neighbor.setTurnout(citizen.turnout);
    }
  }


  // Sub-Activity: Election!
  public void election() {
    leftTotal=0;
    rightTotal=0;
    for (int i = 0; i < numCitizens; i++) {
      Citizen citizen = (Citizen)citizens.get(i);  // loop through citizens
      if (citizen.turnout) {
        if (citizen.getVote(leftPlatform,rightPlatform)==0)
          leftTotal++;
        else
          rightTotal++;
      }
    }
  }

  // Sub-Activity: Record Stats for the No Vote simulation
  public void recordNoVoteStats() {
    leftTotalNV=leftTotal;
    rightTotalNV=rightTotal;
  }

  // Sub-Activity: update stats on difference between vote and no vote election
  public void updateStats() {
    dLeft=leftTotal-leftTotalNV;
    dRight=rightTotal-rightTotalNV;
    if (egoVote==0)
      net=dLeft-dRight;
    else
      net=dRight-dLeft;
    // increment net counter
    netTotal+=net;
  }

  // Sub-Activity: Report Election Results
  public void reportElection() {
    // print results of no vote election
    System.out.println("Turnout: "+(leftTotalNV+rightTotalNV)+" ("
      + new DecimalFormat("##.00").format
      (100.0*((double)leftTotalNV+(double)rightTotalNV)/(double)numCitizens)+"%)  Left: "+leftTotalNV+" ("
      + new DecimalFormat("##.00").format
      (100.0*(double)leftTotalNV/((double)leftTotalNV+(double)rightTotalNV))+"%)  Right: "+rightTotalNV+" ("
      + new DecimalFormat("##.00").format
      (100.0*(double)rightTotalNV/((double)leftTotalNV+(double)rightTotalNV))+"%)");

    // print difference stats
    System.out.println("Turnout: "+(leftTotal+rightTotal-leftTotalNV-rightTotalNV)+" ("
      + new DecimalFormat("##.00").format
      (100.0*((double)(leftTotal+rightTotal-leftTotalNV-rightTotalNV)/(double)numCitizens))+"%)  Left: "+dLeft+" ("
      + new DecimalFormat("##.00").format
      (100.0*(double)dLeft/((double)leftTotal+(double)rightTotal))+"%)  Right: "+dRight+" ("
      + new DecimalFormat("##.00").format
      (100.0*(double)dRight/((double)leftTotal+(double)rightTotal))+"%)  Net: "+net);

    System.out.println("First Order Turnout Correlation: "+sampleTurnoutCorr(10000));
    System.out.println("Second Order Turnout Correlation: "+sampleTurnoutCorr2(10000));

  }

  // Sub-Activity: Reset Electorate
  public void resetElectorate() {
    for (int i = 0; i < numCitizens; i++) {
      Citizen citizen = (Citizen)citizens.get(i);  // loop through citizens
      citizen.setTurnout(citizen.initialTurnout);  // reset to initial turnout
    }
  }

  // After simulation finishes
  public void atEnd() {
    // report average net change in election
    System.out.println("NET: "+(double)netTotal/(double)numEgoSample);
    // check post-electoral turnout corelation

  }

/////////////////////////////////////////////////////////////////////////
// Creating and starting the model /////////////////////////////////////
/////////////////////////////////////////////////////////////////////////
  public static void main(String[] args) {
    SimInit init = new SimInit();
    Model m = new Model();
    init.loadModel(m, null, false);
  }
}